package bmaso.purest;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static bmaso.purest.UriUtils.*;

/**
 * <p>
 * An instance of this class is used to match a URI segment against
 * an expected pattern. The pattern may optionally include matrix parameters.
 * The ignoring matrix parameters for the moment, path patterns may be expressed as:
 * </p>
 * 
 * <ul>
 *   <li><tt><b>"static-content"</b></tt> Static content is matched
 *       exactly. No name/value binding is created when a successful
 *       match is made. This is shorthand for <tt><b>"{:escaped-static-content}"</b></tt>,
 *       where <i>escaped-static-content</i> is the regular-expression-escaped
 *       encoding of the static string <tt><b>"static-content"</b></tt></li>
 *   <li><tt><b>"{:?}"</b></tt> Any segment content will successfully match
 *       against this segment pattern. No bindings are created.</li>
 *   <li><tt><b>"{:regex-pattern}"</b></tt> Content is matched against
 *       the <i>regex-pattern</i> regular expression. The match is
 *       only successful if the segment content matches the expected
 *       pattern. No binding is made even if the segment matches.</li>
 *   <li><tt><b>"{simpleBinding}"</b></tt> A binding is made to the
 *       name <i>simpleBinding</i>, the value is the contents of the
 *       segment. The value can be anything. This is shorthand for
 *       <tt><b>"{simpleBinding:?}"</b></tt></li>
 *   <li><tt><b>"{patternBinding:regex-pattern}"</b></tt> If the segment
 *       matches the <i>regex-pattern</i> regular expression, then a
 *       binding is made with the name <i>patternBinding</i> with the
 *       value of the segment content.</li>
 * </ul>
 * 
 * <p>
 * Any of the patterns above may be appended with optional or required
 * matrix parameters. Each matrix parameter must be present. Optional matrix
 * parameters may be present. If an optional value is present, then it must match
 * the given pattern.
 * </p>
 * 
 * <p>
 * A matrix parameter is indicated by adding a "...;{name:pattern}..." portion
 * to the path segment pattern. Here are some illustrative examples:
 * </p>
 * 
 * <ul>
 *   <li><tt><b>static;{name}</b></tt> The path portion on the segment must
 *       be "static", and there must be a matric parameter named "name" whose
 *       value can be any string (even the empty string). This pattern will
 *       match the first path segment of the following URIs:
 *       <ul>
 *         <li><tt><b>/static;name=x/someOtherPart</b></tt> The request attribute <i>name</i>
 *             will be bound with the value "x"</li>
 *         <li><tt><b>/static;name=XYZ;someOtherMatrixParam=PDQ/someOtherPart</b></tt></li>
 *             The request parameter <i>name</i> will be bound with the value
 *             "XYZ". The second matrix parameter <i>someOtherMatrixParam</i>
 *             will be ignored.</li>
 *       </ul>
 *       <p>Note that this matric parameter pattern is equivalent to the patterns
 *       <tt><b>static;{name:}</b></tt> and <tt><b>static;{name:?}</b>.</p>
 *   </li>
 *   <li><tt><b>static;{name:someValue}</b></tt> The path portion of the segment
 *       must be "static", and there must be a matrix parameter named "name" whose
 *       value matches the static string "someValue". This pattern will match
 *       the first path segment of the following URIs:
 *       <ul>
 *         <li><tt><b>/static;name=someValue</b></tt> The request attribute <i>name</i>
 *             will be bound with the value "someValue".</li>
 *         <li><tt><b>/static;someOtherName=XYZ;name=someValue;somethingElse=Blepharospasm</b></tt>
 *             The request attribute <i>name</i> will be bound with the value "someValue".
 *             The first and third matrix parameters (<i>someOtherName</i> and <i>
 *             somethingElse</i>) will be ignored.</li>
 *       </ul>
 *   </li>
 *   <li><tt><b>flights;{from:[A-Z]+};{to:[A-Z]+}</b></tt> The path portion of the segment
 *       must be "flights", and there must be matrix parameters named "from" and "to" whose
 *       value is a string made up of one or more capital letter characters". This pattern
 *       will match the first path segment of the following URIs:
 *       <ul>
 *         <li><tt><b>/flights;from=LAX;to=JFK</b></tt> The request attribute <i>from</i>
 *             will be bound with the value "LAX", and the request parameter "to" will
 *             be bound with the value "JFK".</li>
 *         <li><tt><b>/flights;to=JKF;withMovie=Sleepless%32in%32Seattle;from=STL</b></tt>
 *             The request attribute <i>from</i> will be bound with the value "STL", and
 *             the request attribute <i>to</i> will be bound with the value "STL" The
 *             "withMovie" matrix parameter that has the value "Sleepless in Seattle"
 *             will be ignored.</li>
 *       </ul>
 *   </li>
 * </ul>
 * 
 * <p>
 * Note that <i>any</i> of the patterns above for the path segment may be used in conjunction
 * with any of these matrix patterns. For example, here is a segment pattern that matches
 * any segment that has a matrix parameter named "pageno" that is a series of digits:
 * </p>
 * 
 *  <p><tt><b>{:?};{pageno}</b></tt></p>
 * 
 * <p>
 * To make a matrix parameter optional, prefix the matrix parameter pattern with the character
 * '?'. If the matrix parameter is not present in an request URI then the declaration of the
 * matrix parameter will be ignored. If the matrix parameter is present in a request URI, then
 * its value must match the pattern or else the URI will not match.
 * </p>
 * 
 * <p>
 * For example, here is a previous example with a new optional parameter:
 * </p>
 * 
 * <p><tt><b>flights;{from:[A-Z]+};{to:[A-Z]+};?{withMovie}</b></tt></p>
 *   
 */
public class SegmentPattern {
	private static final int STATIC_PATTERN = 1;
	private static final int ANONYMOUS_WILDCARD_PATTERN = 2;
	private static final String ANONYMOUS_WILDCARD_STRREGEX =
		"\\{:\\?\\}";
	private static final int ANONYMOUS_REGEX_PATTERN = 3;
	private static final String ANONYMOUS_REGEX_STRREGEX =
		"\\{:([^}]*)\\}";
	private static final int BINDING_WILDCARD_PATTERN = 4;
	private static final String BINDING_WILDCARD_REGEX =
		"\\{([^:]+):\\?\\}";
	private static final int BINDING_REGEX_PATTERN = 5;
	private static final String BINDING_REGEX_STRREGEX =
		"\\{([^:]+):([^}]*)\\}";
	
	private Pattern segmentPattern;
	private Matcher segmentMatcher;
	private boolean fBinds = false;
	private String bindingName;
	private String matchedValue;
	private Map<String, NameValuePattern> mapMatrixParameterPatterns =
		new HashMap<String, NameValuePattern>();
	
	private static final Pattern anonymousWildcardPattern =
		Pattern.compile(ANONYMOUS_WILDCARD_STRREGEX);
	private static final Pattern anonymousRegexPattern =
		Pattern.compile(ANONYMOUS_REGEX_STRREGEX);
	private static final Pattern bindingWildcardPattern =
		Pattern.compile(BINDING_WILDCARD_REGEX);
	private static final Pattern bindingRegexPattern = 
		Pattern.compile(BINDING_REGEX_STRREGEX);
	
	private static final Pattern matrixMembersRegexPattern =
		Pattern.compile("(;\\??\\{[^}]+\\})+$");
	private static final Pattern matrixMemberRegexPattern =
		Pattern.compile(";(\\??\\{[^}]+\\})");
	
	public SegmentPattern(String pat) {
		reset();

		initPathPortionOfPattern(pathPortion(pat));

		String matrixParametersPortion = matrixParametersPortion(pat);
		Matcher m = matrixMemberRegexPattern.matcher(matrixParametersPortion);
		while(m.find()) {
			NameValuePattern nvp = new NameValuePattern(m.group(1));
			mapMatrixParameterPatterns.put(nvp.getParameterName(), nvp);
		}
	}
	
	private String pathPortion(String segmentPattern) {
		Matcher m = matrixMembersRegexPattern.matcher(segmentPattern);
		if(m.find()) {
			return segmentPattern.substring(0, m.start());
		} else {
			return segmentPattern;
		}
	}
	
	private String matrixParametersPortion(String segmentPattern) {
		Matcher m = matrixMembersRegexPattern.matcher(segmentPattern);
		if(m.find()) {
			return m.group();
		} else {
			return "";
		}
	}
	
	private void initPathPortionOfPattern(String pathPortion) {
		switch(pathPatternType(pathPortion))
		{
		case STATIC_PATTERN:
		{
			pathPortion = urlunencode(pathPortion);
			String encodeForEmbeddedRegex = encodeForEmbeddedRegex(pathPortion);
			segmentPattern = Pattern.compile(encodeForEmbeddedRegex);
			break;
		}
			
		case ANONYMOUS_WILDCARD_PATTERN:
			segmentPattern = Pattern.compile(".*");
			break;
			
		case ANONYMOUS_REGEX_PATTERN:
		{
			Matcher m = anonymousRegexPattern.matcher(pathPortion);
			m.matches();
			segmentPattern = Pattern.compile(m.group(1));
			break;
		}
		
		case BINDING_REGEX_PATTERN:
		{
			fBinds = true;
			Matcher m = bindingRegexPattern.matcher(pathPortion);
			m.matches();
			
			bindingName = m.group(1);
			segmentPattern = Pattern.compile(m.group(2));
			break;
		}
		
		case BINDING_WILDCARD_PATTERN:
		{
			fBinds = true;
			Matcher m = bindingWildcardPattern.matcher(pathPortion);
			m.matches();
			
			bindingName = m.group(1);
			segmentPattern = Pattern.compile(".*");
			break;
		}
			
			
		default:
			throw new IllegalURIPatternException("The segment pattern " +
					pathPortion + " is not recognized as a valid segment pattern");
		}
	}

	public boolean match(String segment) {
		reset();
		
		String path = segmentPathWithoutMatrixParameters(segment);
		
		segmentMatcher = segmentPattern.matcher(path);
		boolean fMatches = segmentMatcher.matches();
		if(fMatches)
		{
		  matchedValue = segmentMatcher.group(0);
		  if(bindingName != null) {
			  fBinds = true;
		  }
		  
		  Map<String, String> m = segmentMatrixPatameterNamesAndValues(segment);
		  for(Map.Entry<String, String> me : m.entrySet()) {
			  String mpName = me.getKey();
			  String mpValue = me.getValue();
			  
			  NameValuePattern nvp = mapMatrixParameterPatterns.get(mpName);
			  if(nvp != null && !nvp.matches(mpValue)) {
				  fMatches = false;
				  break;
			  }
		  }
		}
		
		for(NameValuePattern nvp : mapMatrixParameterPatterns.values()) {
			if(!nvp.isOptional() && nvp.getMatchedValue() == null) {
				fMatches = false;
				break;
			}
		}
		
		if(!fMatches) {
			reset();
		}
		
		return fMatches;
	}
	
	public void reset()
	{
		fBinds = false;
		matchedValue = null;
	}
	
	private String encodeForEmbeddedRegex(String unencoded)
	{
		unencoded = unencoded
	        .replace("\\", "\\\\")
		    .replace("[", "\\[")
		    .replace("]", "\\]")
		    .replace(")", "\\)")
		    .replace("(", "\\(")
		    .replace("^", "\\^")
		    .replace("$", "\\$")
		    .replace("?", "\\?")
		    .replace("*", "\\*")
		    .replace("+", "\\+")
		    .replace("|", "\\|")
		    .replace(".", "\\.");
		return unencoded;
	}
	
	private int pathPatternType(String pat)
	{
		if(anonymousWildcardPattern.matcher(pat).matches())
		{
			return ANONYMOUS_WILDCARD_PATTERN;
		} else if(bindingWildcardPattern.matcher(pat).matches())
		{
			return BINDING_WILDCARD_PATTERN;
		} else if(anonymousRegexPattern.matcher(pat).matches())
		{
			return ANONYMOUS_REGEX_PATTERN;
		} else if(bindingRegexPattern.matcher(pat).matches())
		{
			return BINDING_REGEX_PATTERN;
		}
		
		return STATIC_PATTERN; 
	}

	public boolean isBinding() {
		return fBinds;
	}

	public String getBindingName() {
		return bindingName;
	}

	public String getMatchedValue() {
		return matchedValue;
	}
	
	/**
	 * Returns an unmodifyable view of the internal name->name-value-pattern
	 * map.
	 */
	public Map<String, NameValuePattern> getMatrixParameterNameValuePatternMap() {
		return Collections.unmodifiableMap(mapMatrixParameterPatterns);
	}	
}
